﻿/************************************************************************

   Extended Silverlight Toolkit

   Copyright (C) 2010-2012 Xceed Software Inc.

   This program is provided to you under the terms of the Microsoft Public
   License (Ms-PL) as published at http://extendedsilverlight.codeplex.com/license

   Please purchase a commercial version of this toolkit if you are using
   it in a commercial product. The commercial versions support the project,
   and also include more features for each control, priority support,
   source code, updates, removed watermark, and a proprietary license.

   Visit http://xceed.com and follow @datagrid on Twitter.

  **********************************************************************/

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Windows.Threading;

namespace Xceed.Silverlight.Data
{
  internal sealed class BufferingEnumerator : GroupCacheEnumeratorBase
  {
    #region Static Fields

    private static readonly TimeSpan ActivePreFetchingIdleInterval = TimeSpan.FromMilliseconds( 50 );

    private static readonly Guid JumpMoveAndFetchCallId = DataUtils.CreateCallId();
    private static readonly Guid PassivePreFetchingCallId = DataUtils.CreateCallId();
    private static readonly Guid ActivePreFetchingCallId = DataUtils.CreateCallId();

    #endregion

    #region Constructor

    internal BufferingEnumerator(
      IAsyncEnumerator<DataPath> childEnumerator,
      IBufferConfigurable bufferManager,
      BufferDataHelper dataHelper )
      : base( childEnumerator, dataHelper, dataHelper, dataHelper, 0 )
    {
      if( dataHelper == null )
        throw new ArgumentNullException( "dataHelper" );

      m_bufferManager = bufferManager;
      m_dataHelper = dataHelper;

      m_activePreFetchingTimer.Interval = BufferingEnumerator.ActivePreFetchingIdleInterval;
      m_activePreFetchingTimer.Tick += new EventHandler( this.OnStartPreFetching );
    }

    #endregion

    public override JumpMoveAndFetchAsyncResult BeginJumpMoveAndFetch(
      JumpMoveAndFetchParameters parameters,
      AsyncCallback callback,
      object state )
    {
      JumpMoveAndFetchAsyncResult asyncResult = new JumpMoveAndFetchAsyncResult(
                                                  BufferingEnumerator.JumpMoveAndFetchCallId,
                                                  callback,
                                                  state,
                                                  parameters );

      // Try to retrieve the items from the buffer.
      bool isInBuffer = this.PeekBuffer( parameters, asyncResult );

      if( !isInBuffer )
      {
        // Some items aren't in the buffer.  We'll have to query the underlying enumerator
        // to retrieve the requested items.
        this.EnqueueQuery( parameters, asyncResult );
      }

      return asyncResult;
    }

    public override JumpMoveAndFetchResult<DataPath> EndJumpMoveAndFetch(
      JumpMoveAndFetchAsyncResult asyncResult )
    {
      asyncResult.ValidateCallEnd( BufferingEnumerator.JumpMoveAndFetchCallId );

      return asyncResult.GetCallResult<JumpMoveAndFetchResult<DataPath>>();
    }

    protected override void TrimCache(
      int removeCount,
      int topLimitIndex,
      out int removeFromStart,
      out int removeFromEnd )
    {
      lock( this.SyncRoot )
      {
        if( ( removeCount == 0 )
          || ( m_centerOfGravity == null ) )
        {
          base.TrimCache(
            removeCount,
            topLimitIndex,
            out removeFromStart,
            out removeFromEnd );

          return;
        }

        int cacheSize = this.CachedItems.Count;
        int sourcePositionIndex = this.FindIndex( this.GetCurrentContext(), m_centerOfGravity, 0, 1 ).GetValueOrDefault( -1 );

        if( ( sourcePositionIndex < 0 )
          || ( sourcePositionIndex >= cacheSize )
          || ( cacheSize - removeCount <= 0 ) )
        {
          base.TrimCache(
            removeCount,
            topLimitIndex,
            out removeFromStart,
            out removeFromEnd );

          return;
        }
        else if( ( cacheSize - removeCount ) == 1 )
        {
          removeFromStart = sourcePositionIndex;
          removeFromEnd = removeCount - removeFromStart;

          return;
        }

        // Remove the right amount of items to get the selected item near the center 
        // of gravity.
        int expectedSourcePositionIndex = ( int )Math.Ceiling( m_bufferInfo.CenterOfGravity * ( cacheSize - removeCount ) );
        removeFromStart = Math.Min( sourcePositionIndex - expectedSourcePositionIndex, removeCount );
        removeFromEnd = Math.Min( removeCount - removeFromStart, removeCount );

        // Make sure we aren't removing none existing items.
        if( removeFromStart < 0 )
        {
          removeFromStart = 0;
        }
        else if( removeFromEnd < 0 )
        {
          removeFromStart += -removeFromEnd;
        }

        removeFromEnd = ( removeCount - removeFromStart );
      }
    }

    protected sealed override DataSourceContext GetCurrentContext()
    {
      return m_dataHelper.GetContext();
    }

    internal override void ExpansionChanging( ExpansionAction action, DataPath path )
    {
      base.ExpansionChanging( action, path );

      lock( this.SyncRoot )
      {
        if( m_centerOfGravity != null )
        {
          DataPath centerOfGravity = ( DataPath )m_centerOfGravity.Item;

          if( ( path != null ) && ( path.IsAncestorOf( centerOfGravity ) ) )
          {
            m_centerOfGravity = new ItemPosition( path );
          }
          else if( !this.CachedItems.Contains( centerOfGravity ) )
          {
            m_centerOfGravity = null;
          }
        }

        if( ( m_centerOfGravity == null ) && ( this.CachedItems.Count > 0 ) )
        {
          int center = this.CachedItems.Count / 2;

          m_centerOfGravity = new ItemPosition( this.CachedItems[ center ] );
        }
      }
    }

    private static int GetFetchDirection( long moveSize, int fetchSize )
    {
      return ( fetchSize != 0 )
               ? DataUtils.GetUnit( fetchSize )
               : DataUtils.GetUnit( moveSize );
    }

    private static bool IsValuableFetch( int fetchSize )
    {
      return Math.Abs( fetchSize ) > 1;
    }

    private static double RoundTowardZero( double value )
    {
      if( value < 0d )
        return Math.Ceiling( value );

      return Math.Floor( value );
    }

    private int GetPassiveFetchSize( JumpMoveAndFetchParameters parameters )
    {
      lock( this.SyncRoot )
      {
        BufferInfo bufferInfo = this.GetBufferInfo( parameters.ExpectedFetchCount );
        BufferConfiguration configuration = bufferInfo.BufferConfiguration;

        // Set the new buffer capacity.
        int newCapacity = configuration.FindBufferCapacity( bufferInfo );
        this.SetBufferSize( newCapacity );

        // Retrieve the number of items to query in extra.
        bufferInfo = this.GetBufferInfo();
        int passiveFetchSize = configuration.FindPassivePreFetchSize( bufferInfo );

        int fetchDirection = BufferingEnumerator.GetFetchDirection(
                               parameters.ExpectedMoveCount,
                               parameters.ExpectedFetchCount );

        // Calculate the fetch size to query for.
        int fetchSize = ( Math.Abs( parameters.ExpectedFetchCount ) + passiveFetchSize ) * fetchDirection;
        if( fetchSize == 0 )
        {
          fetchSize = ( parameters.ExpectedFetchCount != 0 )
                        ? parameters.ExpectedFetchCount
                        : Math.Sign( fetchDirection );
        }
        // Even if a passive fetch size has been given, make sure the extra items fetched will fit
        // in the buffer.
        else
        {
          int maximumFetchSize = newCapacity;

          // Take into account the center of gravity.
          if( bufferInfo.Size > 0 )
          {
            double centerOfGravity = bufferInfo.CenterOfGravity;
            double capacityRatio = ( fetchDirection >= 0 ) ? 1d - centerOfGravity : centerOfGravity;

            maximumFetchSize = ( int )Math.Floor( newCapacity * capacityRatio );
            maximumFetchSize = Math.Max( 0, Math.Min( newCapacity, maximumFetchSize ) );
          }

          // The buffer may drop some of the items.
          if( Math.Abs( fetchSize ) > maximumFetchSize )
          {
            fetchSize = maximumFetchSize * fetchDirection;
          }
        }

        if( Math.Abs( fetchSize ) < Math.Abs( parameters.ExpectedFetchCount ) )
        {
          fetchSize = parameters.ExpectedFetchCount;
        }

        return fetchSize;
      }
    }

    private void GetActiveFetchSize( DataSourceContext context, out int moveCount, out int fetchCount )
    {
      lock( this.SyncRoot )
      {
        moveCount = 0;
        fetchCount = 0;

        BufferInfo bufferInfo = this.GetBufferInfo();
        BufferConfiguration configuration = bufferInfo.BufferConfiguration;

        // Set the new buffer capacity.
        int newCapacity = configuration.FindBufferCapacity( bufferInfo );
        this.SetBufferSize( newCapacity );

        // Find out the fetch size that is required to adjust the buffer.
        int sourcePositionIndex;
        int fetchSize = this.GetActivePreFetchSizeFromCenterOfGravity( context, out sourcePositionIndex );

        // There is nothing meaningful to fetch.
        if( !BufferingEnumerator.IsValuableFetch( fetchSize ) )
          return;

        // Retrieve the size allowed for prefetching by the configuration.
        int activePreFetchSize = configuration.FindActivePreFetchSize( bufferInfo );

        // The current fetchSize may include some of the items in the buffer.
        // We must find out if the number of items that aren't in the buffer
        // is greater than the allowed fetch size.
        int direction = Math.Sign( fetchSize );
        int itemsInBuffer = ( direction >= 0 )
                              ? ( this.CachedItems.Count - sourcePositionIndex )
                              : sourcePositionIndex + 1;
        int maximumFetchSize = Math.Min( activePreFetchSize, Math.Abs( fetchSize ) - itemsInBuffer );

        // Make sure prefetching is still meaningful.
        if( !BufferingEnumerator.IsValuableFetch( maximumFetchSize ) )
          return;

        moveCount = itemsInBuffer * direction;
        fetchCount = maximumFetchSize * direction;
      }
    }

    private int GetActivePreFetchSizeFromCenterOfGravity( DataSourceContext context, out int sourcePositionIndex )
    {
      lock( this.SyncRoot )
      {
        sourcePositionIndex = -1;

        int maximumCacheSize = this.CacheSize;
        int cacheSize = this.CachedItems.Count;

        // Make sure prefetching is still meaningful.
        if( ( m_centerOfGravity == null )
          || ( maximumCacheSize < cacheSize )
          || ( maximumCacheSize < 3 )
          || ( ( this.IsCacheAtStart ) && ( this.IsCacheAtEnd ) ) )
          return 0;

        // Make sure the center of gravity is within the buffer.
        sourcePositionIndex = this.FindIndex( context, m_centerOfGravity, 0, 1 ).GetValueOrDefault( -1 );
        if( ( sourcePositionIndex < 0 ) || ( sourcePositionIndex >= cacheSize ) )
          return 0;

        BufferInfo bufferInfo = this.GetBufferInfo();
        double centerOfGravity = bufferInfo.CenterOfGravity;
        int centerOfGravityIndex = ( int )Math.Floor( centerOfGravity * ( maximumCacheSize - 1 ) );
        int centerOfGravityToStart = -centerOfGravityIndex - 1;
        int centerOfGravityToEnd = maximumCacheSize - centerOfGravityIndex;
        int fetchSize;

        // The buffer is allowed to grow further.
        if( cacheSize < maximumCacheSize )
        {
          int freeSpaceFromStart = centerOfGravityIndex - sourcePositionIndex;
          int freeSpaceToEnd = maximumCacheSize - ( centerOfGravityIndex + ( cacheSize - sourcePositionIndex ) );

          fetchSize = ( freeSpaceFromStart > freeSpaceToEnd )
                        ? centerOfGravityToStart
                        : centerOfGravityToEnd;
        }
        // Since the buffer is full, we will calculate the fetch size that is required
        // to place the target item in the center of gravity position once the buffer
        // is trimmed.
        else
        {
          double relativePosition = sourcePositionIndex / ( double )( maximumCacheSize - 1 );
          double relativeOffset = relativePosition - centerOfGravity;
          double centerOfGravityThreshold = bufferInfo.CenterOfGravityThreshold;

          // Active prefetching isn't required if the target item is near the
          // center of gravity's location.
          if( ( relativeOffset == 0d ) || ( Math.Abs( relativeOffset ) < centerOfGravityThreshold ) )
            return 0;

          int offset = ( int )BufferingEnumerator.RoundTowardZero( relativeOffset * maximumCacheSize );
          if( offset == 0 )
            return 0;

          fetchSize = ( relativeOffset < 0d ) ? centerOfGravityToStart : centerOfGravityToEnd;
          fetchSize += offset;
        }

        // The center of gravity doesn't need to be respected when the targeted item is between
        // the center of gravity and the start of the source.
        if( this.IsCacheAtStart )
        {
          fetchSize = Math.Max( fetchSize, maximumCacheSize - sourcePositionIndex );
        }
        // The center of gravity doesn't need to be respected when the targeted item is between
        // the center of gravity and the end of the source.
        else if( this.IsCacheAtEnd )
        {
          fetchSize = Math.Min( fetchSize, cacheSize - maximumCacheSize - sourcePositionIndex - 1 );
        }

        return fetchSize;
      }
    }

    private IAsyncResult BeginPassivePreFetching(
      JumpMoveAndFetchParameters parameters,
      AsyncCallback callback,
      object state )
    {
      int fetchSize = this.GetPassiveFetchSize( parameters );
      JumpMoveAndFetchParameters preFetchParameters = parameters.CreateWithNewFetchCount( fetchSize );

      JumpMoveAndFetchAsyncResult asyncResult = new JumpMoveAndFetchAsyncResult(
                                                  BufferingEnumerator.PassivePreFetchingCallId,
                                                  callback,
                                                  state,
                                                  parameters );

      Watch timer = new Watch();

      // Query the underlying enumerator with a boosted fetch size.
      asyncResult.AddDependentCall(
        ( cb, st ) =>
        {
          // Start the timer that will record the response time of the query.
          timer.Start();

          return base.BeginJumpMoveAndFetch( preFetchParameters, cb, st );
        },
        ( par, iar ) =>
        {
          JumpMoveAndFetchResult<DataPath> childResult = base.EndJumpMoveAndFetch( ( JumpMoveAndFetchAsyncResult )iar );

          if( ( m_disposed ) || ( childResult == null ) )
            return new DependentCallResult( null );

          // Keep some statistics about the query for configuration purpose.
          TimeSpan responseTime = timer.Stop();
          this.RecordRequest(
            new PassivePreFetchingBufferRequestInfo(
              childResult.ExpectedFetchCount,
              parameters.ExpectedFetchCount,
              responseTime,
              DateTime.Now ) );

          RelativeOffset endPositionOffset = childResult.EndPositionOffset;
          SourcePosition endPosition = childResult.EndPositionObject;
          bool result = childResult.Result;
          ICollection<DataPath> fetchedItems = childResult.FetchedItems;

          // Remove the items that were fetched in extra in order to return only 
          // the requested items.
          if( ( childResult.FetchedItems != null ) && ( childResult.FetchedItems.Count > 0 ) )
          {
            if( Math.Abs( asyncResult.ExpectedFetchCount ) <= childResult.FetchedItems.Count )
            {
              if( asyncResult.ExpectedFetchCount == 0 )
              {
                fetchedItems = new List<DataPath>();

                if( childResult.FetchedItems.Count > 0 )
                {
                  endPosition = new ItemPosition( childResult.FetchedItems.First() );
                  endPositionOffset = DataUtils.CalculateRelativeOffset( endPositionOffset, -childResult.GetFetchDisplacement() );
                }
              }
              else
              {
                List<DataPath> itemList = childResult.FetchedItems.Take( Math.Abs( asyncResult.ExpectedFetchCount ) ).ToList();
                int expectedFetchDisplacement = asyncResult.ExpectedFetchCount - asyncResult.GetFetchUnit();

                endPosition = new ItemPosition( itemList[ itemList.Count - 1 ] );
                endPositionOffset = DataUtils.CalculateRelativeOffset( endPositionOffset, -( childResult.GetFetchDisplacement() - expectedFetchDisplacement ) );
                fetchedItems = itemList;
              }

              result = true;
            }
          }

          // Update the center of gravity.
          this.UpdateCenterOfGravity( 
            parameters.Context,
            endPosition as ItemPosition, 
            fetchedItems );

          return new DependentCallResult(
            new JumpMoveAndFetchResult<DataPath>(
              asyncResult,
              childResult.ActualMoveCount,
              fetchedItems,
              result,
              endPosition,
              endPositionOffset,
              childResult.TotalCount ) );
        } );

      asyncResult.Start();

      return asyncResult;
    }

    private JumpMoveAndFetchResult<DataPath> EndPassivePreFetching( IAsyncResult asyncResult )
    {
      asyncResult.ValidateCallEnd( BufferingEnumerator.PassivePreFetchingCallId );

      return asyncResult.GetCallResult<JumpMoveAndFetchResult<DataPath>>();
    }

    private bool PeekBuffer(
      JumpMoveAndFetchParameters parameters,
      JumpMoveAndFetchAsyncResult asyncResult )
    {
      long actualMove;
      IList<DataPath> items;
      SourcePosition endPosition;
      Nullable<int> sourcePositionIndex;
      bool operationCompleted;
      RelativeOffset endPositionOffset;
      EstimateInt64 totalCount;

      Watch timer = new Watch();
      timer.Start();

      bool result = this.CacheJumpMoveAndFetch(
                      parameters,
                      out actualMove,
                      out items,
                      out endPosition,
                      out sourcePositionIndex,
                      out operationCompleted,
                      out endPositionOffset,
                      out totalCount );

      TimeSpan responseTime = timer.Stop();

      // The buffer doesn't contain all the requested data.
      if( !operationCompleted )
        return false;

      DataSourceContext context = this.GetContext( parameters );
      bool isFetchingForward = ( BufferingEnumerator.GetFetchDirection( parameters.ExpectedMoveCount, parameters.ExpectedFetchCount ) >= 0 );
      int inlineCount = this.GetItemsCountFromCenterOfGravity( context, isFetchingForward );

      this.RecordRequest(
        new SuccessBufferRequestInfo(
          parameters.ExpectedFetchCount,
          inlineCount,
          responseTime,
          DateTime.Now ) );

      lock( this.SyncRoot )
      {
        BufferInfo bufferInfo = this.GetBufferInfo();
        BufferConfiguration configuration = bufferInfo.BufferConfiguration;

        // Set the new buffer capacity.
        int newCapacity = configuration.FindBufferCapacity( bufferInfo );
        this.SetBufferSize( newCapacity );

        // Update the center of gravity.
        this.UpdateCenterOfGravity( context, endPosition as ItemPosition, items );
      }

      // Start prefetching actively in order to move the center of gravity.
      this.StartPreFetching();

      // Return the data that was already in the buffer.
      JumpMoveAndFetchResult<DataPath> queryResult = new JumpMoveAndFetchResult<DataPath>(
                                                       asyncResult,
                                                       actualMove,
                                                       items,
                                                       result,
                                                       endPosition,
                                                       endPositionOffset,
                                                       totalCount );

      asyncResult.StartSync( queryResult );

      return true;
    }

    private void EnqueueQuery(
      JumpMoveAndFetchParameters parameters,
      JumpMoveAndFetchAsyncResult asyncResult )
    {
      lock( this.SyncRoot )
      {
        if( m_disposed )
          return;

        bool isQueryRunnable = !this.IsRunning;

        m_requestsQueue.AddLast( asyncResult );

        // The query may be launched right now only if no other query is running.
        if( isQueryRunnable )
        {
          this.DequeueQuery( false );
        }
      }
    }

    private void DequeueQuery( bool isAsyncResult )
    {
      JumpMoveAndFetchAsyncResult asyncResult;

      lock( this.SyncRoot )
      {
        if( ( m_disposed )
          || ( m_requestsQueue.Count <= 0 )
          || ( this.IsRunning ) )
          return;

        asyncResult = m_requestsQueue.First.Value;

        this.SetRunningOperation( asyncResult );
      }

      JumpMoveAndFetchParameters parameters = asyncResult.GetParameters();

      // Some queries have modified the buffer since last time.  Check out the buffer
      // first in case the requested data is in there.
      if( isAsyncResult )
      {
        JumpMoveAndFetchAsyncResult cachedAsyncResult = new JumpMoveAndFetchAsyncResult( asyncResult.CallId, null, null, parameters );

        // The requested data is in the buffer.
        if( this.PeekBuffer( parameters, cachedAsyncResult ) )
        {
          Debug.Assert( cachedAsyncResult.CompletedSynchronously );

          lock( this.SyncRoot )
          {
            m_requestsQueue.RemoveFirst();
          }

          this.SetRunningOperation( null );

          JumpMoveAndFetchResult<DataPath> cachedResult = cachedAsyncResult.GetCallResult<JumpMoveAndFetchResult<DataPath>>();

          // Schedule the next query.
          this.StartNextQuery();

          asyncResult.StartAsync( cachedResult );

          return;
        }
      }

      // Start the query that will get the requested items from the underlying enumerator.
      JumpMoveAndFetchAsyncResult passiveAsyncResult = new JumpMoveAndFetchAsyncResult( asyncResult.CallId, null, null, parameters );
      passiveAsyncResult.Start(
        ( cb, st ) => this.BeginPassivePreFetching( parameters, cb, st ),
        ( par, iar ) =>
        {
          lock( this.SyncRoot )
          {
            if( m_requestsQueue.Any() )
            {
              m_requestsQueue.RemoveFirst();
            }
          }

          JumpMoveAndFetchResult<DataPath> result;

          try
          {
            result = this.EndPassivePreFetching( iar );
          }
          finally
          {
            this.SetRunningOperation( null );
          }

          if( ( m_disposed ) || ( result == null ) )
            return new DependentCallResult( null );

          // Schedule the next query.
          this.StartNextQuery();

          if( ( iar.CompletedSynchronously ) && ( !isAsyncResult ) )
          {
            asyncResult.StartSync( result );
          }
          else
          {
            asyncResult.StartAsync( result );
          }

          return new DependentCallResult( null );
        } );
    }

    private void PreFetch()
    {
      JumpMoveAndFetchAsyncResult asyncResult;
      JumpMoveAndFetchParameters parameters;

      lock( this.SyncRoot )
      {
        if( !this.IsActivePreFetchingRunnable() )
          return;

        DataSourceContext context = this.GetCurrentContext();
        int moveSize;
        int fetchSize;
        this.GetActiveFetchSize( context, out moveSize, out fetchSize );

        // Make sure the fetch size is meaningful.
        if( fetchSize == 0 )
          return;

        parameters = new JumpMoveAndFetchParameters( m_centerOfGravity, moveSize, fetchSize, context );
        asyncResult = new JumpMoveAndFetchAsyncResult(
                        BufferingEnumerator.ActivePreFetchingCallId,
                        this.PreFetchCallback,
                        null,
                        parameters );

        // Set the current query as the running query if possible.
        this.SetRunningOperation( asyncResult );
      }

      Watch timer = new Watch();

      asyncResult.Start(
        ( cb, st ) =>
        {
          // Start the timer that will record the response time of the query.
          timer.Start();

          return base.BeginJumpMoveAndFetch( parameters, cb, st );
        },
        ( par, iar ) =>
        {
          JumpMoveAndFetchResult<DataPath> result;

          try
          {
            result = base.EndJumpMoveAndFetch( ( JumpMoveAndFetchAsyncResult )iar );
          }
          finally
          {
            this.SetRunningOperation( null );
          }

          if( ( m_disposed ) || ( result == null ) )
            return new DependentCallResult( null );

          // Keep some statistics about the query for configuration purpose.
          TimeSpan responseTime = timer.Stop();
          int fetchCount = ( result.FetchedItems != null ) ? result.FetchedItems.Count : 0;

          this.RecordRequest(
            new ActivePreFetchingBufferRequestInfo(
              fetchCount,
              responseTime,
              DateTime.Now ) );

          return new DependentCallResult( result );
        } );

      if( ( asyncResult.IsCompleted ) && ( asyncResult.CompletedSynchronously ) )
      {
        this.StartNextQuery();
      }
    }

    private void PreFetchCallback( IAsyncResult asyncResult )
    {
      if( ( asyncResult.IsCompleted ) && ( asyncResult.CompletedSynchronously ) )
        return;

      this.StartNextQuery();
    }

    private void StartNextQuery()
    {
      lock( this.SyncRoot )
      {
        if( m_requestsQueue.Count == 0 )
        {
          this.StartPreFetching();
        }
        else
        {
          this.StartFetching();
        }
      }
    }

    private void SetBufferSize( int capacity )
    {
      this.SetCacheSize( capacity, 0 );
    }

    private BufferInfo GetBufferInfo()
    {
      return this.GetBufferInfo( this.CacheSize );
    }

    private BufferInfo GetBufferInfo( int fetchSize )
    {
      if( m_bufferInfo != null )
      {
        lock( this.SyncRoot )
        {
          int cacheCapacity = Math.Max( this.CacheSize, Math.Abs( fetchSize ) );
          int cacheSize = this.CachedItems.Count;

          m_bufferInfo.Update( cacheSize, cacheCapacity, m_requestsHistory );

          // Make sure the buffer info has been initialized by the same BufferConfiguration.
          BufferConfiguration configuration = this.GetBufferConfiguration();
          if( configuration != null )
          {
            BufferConfiguration.Prepare( configuration, m_bufferInfo );
          }
        }
      }

      return m_bufferInfo;
    }

    private BufferConfiguration GetBufferConfiguration()
    {
      BufferConfiguration configuration = null;
      if( m_bufferManager != null )
      {
        configuration = m_bufferManager.BufferConfiguration;
      }

      if( configuration == null )
      {
        lock( this.SyncRoot )
        {
          if( m_bufferConfiguration == null )
          {
            m_bufferConfiguration = new DisabledBufferConfiguration();
          }
        }

        configuration = m_bufferConfiguration;
      }

      return configuration;
    }

    private int GetItemsCountFromCenterOfGravity( DataSourceContext context, bool isFetchingForward )
    {
      lock( this.SyncRoot )
      {
        // Make sure the center of gravity is within the buffer.
        if( m_centerOfGravity != null )
        {
          int cacheSize = this.CachedItems.Count;
          int centerOfGravityIndex = this.FindIndex( context, m_centerOfGravity, 0, 1 ).GetValueOrDefault( -1 );

          if( ( centerOfGravityIndex >= 0 ) && ( centerOfGravityIndex < cacheSize ) )
            return ( isFetchingForward )
                     ? cacheSize - centerOfGravityIndex
                     : centerOfGravityIndex + 1;
        }

        return 0;
      }
    }

    private void UpdateCenterOfGravity( 
      DataSourceContext context, 
      ItemPosition position, 
      ICollection<DataPath> items )
    {
      lock( this.SyncRoot )
      {
        if( this.MatchCurrentContext( context ) )
        {
          if( position != null )
          {
            m_centerOfGravity = position;
          }
          else if( ( items != null ) && ( items.Count > 0 ) )
          {
            m_centerOfGravity = new ItemPosition( items.Last() );
          }
        }
      }
    }

    #region IDisposable Members

    protected override void Dispose( bool disposing )
    {
      lock( this.SyncRoot )
      {
        if( m_bufferInfo != null )
        {
          m_bufferInfo.CleanUp();
        }

        m_bufferInfo = null;
        m_bufferConfiguration = null;
        m_centerOfGravity = null;
        m_requestsQueue.Clear();
        m_requestsHistory.Clear();
        m_disposed = true;
      }

      base.Dispose( disposing );
    }

    #endregion

    #region Request Scheduling Methods

    private bool IsRunning
    {
      get
      {
        lock( this.SyncRoot )
        {
          return ( m_runningOperation != null )
              && ( !m_disposed );
        }
      }
    }

    private void StartFetching()
    {
      this.DequeueQuery( true );
    }

    private void StartPreFetching()
    {
      if( !this.IsActivePreFetchingRunnable() )
        return;

      if( !m_activePreFetchingTimer.IsEnabled )
      {
        m_activePreFetchingTimer.Start();
      }
    }

    private void OnStartPreFetching( object sender, EventArgs e )
    {
      DispatcherTimer timer = ( DispatcherTimer )sender;
      timer.Stop();

      if( m_disposed )
      {
        timer.Tick -= new EventHandler( this.OnStartPreFetching );
        return;
      }

      this.PreFetch();
    }

    private bool IsActivePreFetchingRunnable()
    {
      lock( this.SyncRoot )
      {
        // Make sure the buffer isn't disabled and a query isn't already running
        // and the center of gravity is appropriate.
        return ( !m_disposed )
            && ( this.CacheSize > 0 )
            && ( m_centerOfGravity != null )
            && ( !this.IsRunning );
      }
    }

    private void SetRunningOperation( JumpMoveAndFetchAsyncResult asyncResult )
    {
      lock( this.SyncRoot )
      {
        if( ( asyncResult != null ) && ( this.IsRunning ) )
          throw new InvalidOperationException();

        m_runningOperation = asyncResult;
      }
    }

    private JumpMoveAndFetchAsyncResult m_runningOperation; //null

    #endregion

    #region Request Statistics Methods

    private void RecordRequest( BufferRequestInfo request )
    {
      if( ( request == null ) || ( m_requestsHistory == null ) || ( m_disposed ) )
        return;

      BufferInfo bufferInfo = this.GetBufferInfo();
      if( bufferInfo == null )
        return;

      lock( this.SyncRoot )
      {
        int samplesCount = bufferInfo.RequestsHistoryLength;
        while( ( m_requestsHistory.Count >= samplesCount )
            && ( m_requestsHistory.Count > 0 ) )
        {
          m_requestsHistory.RemoveLast();
        }

        m_requestsHistory.AddFirst( request );
      }
    }

    private LinkedList<BufferRequestInfo> m_requestsHistory = new LinkedList<BufferRequestInfo>();

    #endregion

    #region Private Fields

    private readonly IBufferConfigurable m_bufferManager; //null
    private readonly BufferDataHelper m_dataHelper;
    private readonly LinkedList<JumpMoveAndFetchAsyncResult> m_requestsQueue = new LinkedList<JumpMoveAndFetchAsyncResult>();
    private readonly DispatcherTimer m_activePreFetchingTimer = new DispatcherTimer();
    private BufferConfiguration m_bufferConfiguration; //null
    private BufferInfo m_bufferInfo = new BufferInfo();
    private ItemPosition m_centerOfGravity; //null
    private bool m_disposed; //false

    #endregion

    #region Private Watch Nested Type

    private class Watch
    {
      internal void Start()
      {
        if( m_running )
          throw new InvalidOperationException();

        m_running = true;
        m_startValue = Environment.TickCount;
      }

      internal TimeSpan Stop()
      {
        if( !m_running )
          throw new InvalidOperationException();

        int endValue = Environment.TickCount;
        m_running = false;

        long interval;

        if( endValue >= m_startValue )
        {
          interval = ( endValue - m_startValue );
        }
        else
        {
          interval = ( int.MaxValue - m_startValue ) + ( endValue - int.MinValue );
        }

        return TimeSpan.FromMilliseconds( interval );
      }

      private bool m_running; //false
      private int m_startValue; //0
    }

    #endregion
  }
}
